package nachos.filesys;

import java.util.ArrayList;

import nachos.filesys.DataManager.DirectoryItem;
import nachos.machine.Lib;
import nachos.machine.Machine;


public class Unode {
	private Unode(int fileType){
		uFileType = fileType;
		uFileLimit = 7;
		uFileSize = 0;
		if (fileType == typeDirectory)
			uFileSize = 8;
		uCreateTime = getCurrentTimeString();
		uModifyTime = getCurrentTimeString();
		for (int i = 0; i < uIndexTable.length; i ++){
			uIndexTable[i] = -1;
		}
	}
	
	public static Unode newUnode(int fileType){
		return new Unode(fileType);
	}
	
	/**
	 * Convert byte array to Unode object
	 * @param array the byte array to translate
	 * @return the Unode object
	 */
	public static Unode byteToUnode(byte[] array){
		int type = getUnodeIntInfoFromByte(cFileTypeCode, array);
		Unode unode = new Unode(type);
		unode.setFileLimit(getUnodeIntInfoFromByte(cFileLimitCode, array));
		unode.setFileSize(getUnodeIntInfoFromByte(cFileSizeCode, array));
		unode.uIndexTable = getUnodeIndexTableFromByte(array);
		unode.uCreateTime = getUnodeStringInfoFromByte(cCreateTimeCode, array);
		unode.uModifyTime = getUnodeStringInfoFromByte(cModifyTimeCode, array);
		return unode;
	}
	
	private static int getUnodeIntInfoFromByte(int partCode, byte[] target){
		Lib.assertTrue(partCode == cFileTypeCode || partCode == cFileLimitCode || partCode == cFileSizeCode);
		int pos = findPos(partCode); 		
		byte[] temp = new byte[4];
		System.arraycopy(target, pos, temp, 0, 4);
		return Lib.bytesToInt(temp, 0);
	}
	
	private static String getUnodeStringInfoFromByte(int partCode, byte[] target){
		Lib.assertTrue(partCode == cCreateTimeCode || partCode == cModifyTimeCode);
		int pos = findPos(partCode);
		return Lib.bytesToString(target, pos, sCreateTimeLength);
	}
	
	/**
	 * extract the index table of the Unode object through the byte array 
	 * @param partCode
	 * @param target
	 * @return
	 */
	private static int[] getUnodeIndexTableFromByte(byte[] target){
		int pos = findPos(cIndexTableCode);
		int[] result = new int[10];
		byte[] temp = new byte[4];
		for (int i = 0; i < 10; i ++){
			System.arraycopy(target, pos, temp, 0, 4);
			result[i] = Lib.bytesToInt(temp, 0);			
			pos += 4;
		}
		return result;
	}

	/**
	 * Convert Unode to byte array
	 * @param unode the Unode object to translate
	 * @return the byte array
	 */
	public static byte[] byteFromUnode(Unode unode){
		byte[] result = new byte[sUnodeSize];
		writeUnodeInfoIntoByte(cFileTypeCode, unode.uFileType, result);
		writeUnodeInfoIntoByte(cFileLimitCode, unode.uFileLimit, result);
		writeUnodeInfoIntoByte(cFileSizeCode, unode.uFileSize, result);
		writeUnodeInfoIntoByte(cCreateTimeCode, unode.uCreateTime, result);
		writeUnodeInfoIntoByte(cModifyTimeCode, unode.uModifyTime, result);
		writeUnodeInfoIntoByte(cIndexTableCode, unode.uIndexTable, result);		
		return result;		
	}
	
	/**
	 * write the unode information such as file type, 
	 * file limit and file size into the byte array.
	 * @param partCode
	 * @param value
	 * @param target
	 */
	static private void writeUnodeInfoIntoByte(int partCode, int value, byte[] target){
		Lib.assertTrue(partCode == cFileTypeCode || partCode == cFileLimitCode || partCode == cFileSizeCode);
		byte[] temp = Lib.bytesFromInt(value);
		int pos = findPos(partCode);
		System.arraycopy(temp, 0, target, pos, 4);
	}
	
	/**
	 * write the index table into the unode byte array.
	 * @param partCode
	 * @param value
	 * @param target
	 */
	static private void writeUnodeInfoIntoByte(int partCode, int[] value, byte[] target){
		Lib.assertTrue(partCode == cIndexTableCode);
		int pos = findPos(partCode);
		for (int i = 0; i < value.length; i ++){
			byte[] temp = Lib.bytesFromInt(value[i]);
			System.arraycopy(temp, 0, target, pos+i*4, 4);
		}
	}
	
	/**
	 * write create time or modify time into the unode byte array.
	 * @param partCode
	 * @param str
	 * @param target
	 */
	static private void writeUnodeInfoIntoByte(int partCode, String str, byte[] target){
		Lib.assertTrue(partCode == cCreateTimeCode || partCode == cModifyTimeCode);
		int pos = findPos(partCode);
		byte[] temp = str.getBytes();
		System.arraycopy(temp, 0, target, pos, temp.length);
	}
	
	static private int findPos(int partCode){
		switch (partCode){
		case cFileTypeCode:
			return sFileTypePos;
		case cFileLimitCode:
			return sFileLimitPos;
		case cFileSizeCode:
			return sFileSizePos;
		case cCreateTimeCode:
			return sCreateTimePos;
		case cModifyTimeCode:
			return sModifyTimePos;
		case cIndexTableCode:
			return sIndexTablePos;
		default:
			Lib.assertNotReached();
			return -1;
		}
	}
	
	/**
	 * get file type
	 * @return type
	 */
	public int getFileType(){
		return uFileType;
	}
	
	public int getFileSize(){
		return uFileSize;
	}
	
	public void setFileSize(int size){
		uFileSize = size;
	}
	
	public void increaseFileSize(int inc){
		uFileSize += inc;
		Lib.assertTrue(uFileSize >= 0);
		if (debugFlag)
			this.printInfo("increase file size by " + inc);
	}
	
	public void updateModifyTime(){
		uModifyTime = getCurrentTimeString();
		
	}
	
	/**
	 * get current time in string form like: "yyyy-mm-dd hour:min:sec"
	 * @return time string
	 */
	private static String getCurrentTimeString(){
		long time = Machine.timer().getTime();
		return String.valueOf(time);
	}

	
	/**
	 * file limit setter
	 * @param _fileLimit
	 */
	public void setFileLimit(boolean read, boolean write, boolean exec){
		int res = 0;
		if (read)
			res &= 4;
		if (write)
			res &= 2;
		if (exec)
			res &= 1;
		uFileLimit = res;
	}
	
	public void setFileLimit(int limit){
		uFileLimit = limit;
		return;
	}
	
	/**
	 * get execute limit
	 * @return
	 */
	public boolean getExecLimit(){
		return !((uFileLimit & 1) == 0);
	}
	
	/**
	 * get read limit
	 * @return
	 */
	public boolean getReadLimit(){
		return !((uFileLimit & 4) == 0);
	}
	
	/**
	 * get write limit
	 * @return
	 */
	public boolean getWriteLimit(){
		return !((uFileLimit & 2) == 0);
	}
	
	public int[] getIndexTable(){
		return uIndexTable;
	}
	
	/**
	 * search item in the index table, by name
	 * @param name
	 * @return if exists, return unode number, or return -1 if cannot find.
	 */
	public int searchNameInIndexTable(String targetName){
	
		int blockCount;
		if (this.uFileSize == 0){
			blockCount = 0;
		}
		else {
			blockCount = (this.uFileSize-1) / 1024 + 1;
		}
		for (int i = 0; i < blockCount; i ++){
			int blockNum;
			if (i < 8){
				blockNum = uIndexTable[i];
			}else if (i >= 8 && i < 264){
				blockNum = DirectoryBlockManager.getIndexFromOneLevelBlock(uIndexTable[8], i - 8);
			}else{
				blockNum = DirectoryBlockManager.getIndexFromTwoLevelBlock(uIndexTable[9], i - 264);
			}
			int res = findName(blockNum, targetName); 
			if (res != -1){
				return res;
			}
		}
		return -1;
	}
	
	/**
	 * 
	 * @param blockNum
	 * @param targetName
	 * @return the target's unode number
	 */
	private int findName(int blockNum, String targetName){
		ArrayList<DirectoryItem> al = 
				DataManager.getInstance().getDirectoryItemsByBlockNum(blockNum);
		for (DirectoryItem d : al){
			if (d.name.equals(targetName))
				return d.unodeNum;
		}
		return -1;
	}
	
	/**
	 * find out which physical block this unode's specific block of contents are 
	 * @param pos
	 * @return the physical block number
	 */
	public int searchBlockInIndexTable(int pos){
		
		if (pos < 8){
			return uIndexTable[pos];
		}
		else if (pos >= 8 && pos < 264){
			return DirectoryBlockManager.getIndexFromOneLevelBlock(uIndexTable[8], pos - 8);
		}else if (pos >= 264){
			return DirectoryBlockManager.getIndexFromTwoLevelBlock(uIndexTable[9], pos - 264);
		}else{
			Lib.assertNotReached();
			return -1;
		}		
	}
	
	/**
	 * add a new item into the index table
	 * @param index the absolute physic block number to add 
	 */
	public void addIndexItem(int index){
		
		//find the exact position to add the new index
		int blockCount;
		if (this.uFileSize == 0){
			blockCount = 0;
		}
		else {
			blockCount = (this.uFileSize-1) / 1024 + 1;
		}
		if (blockCount < 8){
			// add directly
			int pos;
			if (uFileType == typeDirectory && uFileSize == 8)
				pos = 0;
			else
				pos = blockCount;
			uIndexTable[pos] = index;
			return;			
		}else if (blockCount == 8){
			//allocate a new 1 level index block
			int pos = DirectoryBlockManager.newOneLevelBlock();
			uIndexTable[8] = pos;
			DirectoryBlockManager.addIndexIntoOneLevelBlock(pos, 0, index);
		}else if (blockCount > 8 && blockCount < 264){
			// add into 1 level index table 
			DirectoryBlockManager.addIndexIntoOneLevelBlock(uIndexTable[8], blockCount - 8, index);			
		}else if (blockCount == 264){
			//allocate a new 2 level index block
			int pos = DirectoryBlockManager.newTwoLevelBlock();
			uIndexTable[9] = pos;
			DirectoryBlockManager.addIndexIntoTwoLevelBlock(pos, 0, index);
		}else if (blockCount > 264){
			// add 2 level index table
			DirectoryBlockManager.addIndexIntoTwoLevelBlock(uIndexTable[9], blockCount - 264, index);
		}		
	}
	
	/**
	 * remove a specific item from the unode's indexTable
	 * @param i the position of the target index item to remove
	 */
	public void removeIndexItem(int index) {		
		if (index < 8){
			uIndexTable[index] = -1;
		}
		else if (index >= 8 && index < 264){
			boolean needRelease = DirectoryBlockManager.removeItemFromOneLevelBlock(uIndexTable[8], index - 8);
			if (needRelease){
				uIndexTable[8] = -1;
			}
		}
		else{
			boolean needRelease = DirectoryBlockManager.removeItemFromTwoLevelBlock(uIndexTable[9], index - 264);
			if (needRelease){
				uIndexTable[9] = -1;
			}
		}
	}
	
	public String getCreateTime(){
		return uCreateTime;
	}
	
	public String getLastModifyTime(){
		return uModifyTime;
	}
	
	public void printInfo(String str){
		System.out.println(str);
		System.out.println("\tfile type = " + getTypeString(uFileType));
		System.out.println("\tfile limit e : " + getExecLimit() + 
				"\tr : " + getReadLimit() + 
				"\tw : " + getWriteLimit());
		System.out.println("\tfile size = " + getFileSize());
		System.out.println("\tcreate time = " + getCreateTime());
		System.out.println("\tlast modify time = " + getLastModifyTime());
		printIndexTable();
	}
	
	public void printIndexTable(){
		System.out.println("\tindex table:");
		for (int i = 0; i < 10; i ++){
			if (uIndexTable[i] == -1)
				break;
			System.out.println("\t\titem" + i + " : " + uIndexTable[i]);
		}
	}
	
	public String getTypeString(int typeInt){
		switch(typeInt){
		case typeDirectory:
			return "directory";
		case typeFile:
			return "file";
		case typeLink:
			return "link";
		default:
			Lib.assertNotReached();
			return null;
		}
	}
	
	static class DirectoryBlockManager{
		/**
		 * allocate a new one level block for the index table
		 * @return the new block's absolute position
		 */
		static int newOneLevelBlock(){
			return DataManager.getInstance().requestFreeBlock(1).get(0);
		}
		
		/**
		 * remove the exact item from a one level block
		 * @param unode
		 * @param index relevant index number of the level one block
		 * @return true if this one level block should be released
		 */
		static boolean removeItemFromOneLevelBlock(int blockNum, int index) {
			// find the target block and remove the target index item
			byte[] temp = new byte[1024];
			diskHelper.readBlock(blockNum, 1, temp);

			// if this block is empty, release it
			boolean empty = true;
			for (int i = 0; i < 1024 / 4; i ++){
				if (Lib.bytesToInt(temp, i*4, 4) != -1){
					empty = false;
					break;
				}					
			}
			if (empty){
				//release the block
				DataManager.getInstance().setDataBitmapFree(blockNum);
				return true;
			}else{
				//set the target index position empty 
				byte[] tempInt = Lib.bytesFromInt(-1);
				System.arraycopy(tempInt, 0, temp, index * 4, 4);
				diskHelper.writeBlock(blockNum, 1, temp);
				return false;
			}
		}

		/**
		 * remove the exact item from a one level block
		 * @param unode
		 * @param index relevant index number of the level two block
		 * @return true if this block need to be released
		 */
		static boolean removeItemFromTwoLevelBlock(int blockNum, int index) {
			byte[] temp = new byte[1024];
			diskHelper.readBlock(blockNum, 1, temp);
			int targetOneLevelBlockIndex = index / 256;
			int targetOneLevelBlockNum = Lib.bytesToInt(temp, targetOneLevelBlockIndex * 4, 4);
			int targetOneLevelBlockOffset = index % 256;
			// need to know whether the one level block need release? 
			boolean res = removeItemFromOneLevelBlock(targetOneLevelBlockNum, targetOneLevelBlockOffset);
			if (res){
				// remove from the two level block
				byte[] tempInt = Lib.bytesFromInt(-1);
				System.arraycopy(tempInt, 0, temp, targetOneLevelBlockIndex * 4, 4);
				
				// if this block is empty too, release it
				boolean empty = true;
				for (int i = 0; i < 1024 / 4; i ++){
					if (Lib.bytesToInt(temp, i*4, 4) != -1){
						empty = false;
						break;
					}					
				}
				if (empty){
					//release the block
					DataManager.getInstance().setDataBitmapFree(blockNum);
					return true;
				}
				else 
					return false;
			}
			else{
				return false;
			}
			
		}

		/**
		 * allocate a new two level block for the index table
		 * @return the new block's absolute position
		 */
		static int newTwoLevelBlock(){
			return DataManager.getInstance().requestFreeBlock(1).get(0);
		}
		
		
		static void addIndexIntoOneLevelBlock(int blockNum, int addPosition, int newIndex){
			byte[] temp = new byte[1024];
			int targetPosition = addPosition * 4;
			byte[] tempInt = Lib.bytesFromInt(newIndex);
			diskHelper.readBlock(blockNum, 1, temp);
			System.arraycopy(tempInt, 0, temp, targetPosition, 4);
			diskHelper.writeBlock(blockNum, 1, temp);
			
		}
		
		static void addIndexIntoTwoLevelBlock(int blockNum, int addPosition, int newIndex){
			byte[] temp = new byte[1024];
			diskHelper.readBlock(blockNum, 1, temp);
			int targetOneLevelBlockIndex = addPosition / 256;
			int targetOneLevelBlockNum = Lib.bytesToInt(temp, targetOneLevelBlockIndex * 4, 4);
			int targetOneLevelBlockOffset = addPosition % 256;
			addIndexIntoOneLevelBlock(targetOneLevelBlockNum, targetOneLevelBlockOffset, newIndex);
		}
		
		static int getIndexFromOneLevelBlock(int blockNum, int index){
			byte[] temp = new byte[1024];
			diskHelper.readBlock(blockNum, 1, temp);
			return Lib.bytesToInt(temp, index * 4, 4);
		}
		
		static int getIndexFromTwoLevelBlock(int blockNum, int index){
			byte[] temp = new byte[1024];
			diskHelper.readBlock(blockNum, 1, temp);
			int oneLevelBlockNum = Lib.bytesToInt(temp, (index / 256) * 4 , 4);
			return getIndexFromOneLevelBlock(oneLevelBlockNum, index % 256);
		}
		
		private static DiskHelper diskHelper = DiskHelper.getInstance();
		
	}
	
	/** unode size */
	private static final int sUnodeSize = 128;
	
	/** file type */
	private final int uFileType;
	public final static int 
		typeFile = 0,
		typeDirectory = 1,
		typeLink = 2;
	
	/** 
	 * file limit, brief description from unix 
	 * description, just 3 bits for exec\read\write
	 * */
	private int uFileLimit;
	
	/** size of this file, in bytes */
	private int uFileSize;
	
	/** create time, format:"yyyy-mm-dd hour:min:sec" */
	private String uCreateTime = null;
	
	private String uModifyTime = null;
	
	/** index table*/
	private int[] uIndexTable = new int[10];
	
	private static final int 
		sFileTypePos = 0,
		sFileLimitPos = 4,
		sFileSizePos = 8,
		sCreateTimePos = 12,
		sCreateTimeLength = 20,
		sModifyTimePos = 32,
		sIndexTablePos = 52;
	
	private static final int
		cFileTypeCode = 0,
		cFileLimitCode = 1,
		cFileSizeCode = 2,
		cCreateTimeCode = 3,
		cModifyTimeCode = 4,
		cIndexTableCode = 5;
	
	private static final boolean debugFlag = false;

}
